﻿/* 
   Copyright 2011 LUCA Studios LLC & John Evans

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

namespace LUCAStudios.Silverlight.Controls.Draggable
{
    using System;
    using System.Collections.Generic;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Linq;

    /// <summary>
    /// Base class providing a centralized dragging service for FrameworkElements
    /// </summary>
    public abstract class DraggableBase : IDisposable
    {
        /// <summary>
        /// Delegate for manaaging different transform operations
        /// </summary>
        /// <param name="f"></param>
        /// <param name="p"></param>
        /// <param name="g"></param>
        protected delegate void TransformDelegate(FrameworkElement f, Point p, TransformGroup g);
        private readonly Dictionary<FrameworkElement, IDraggable> _clickTargetLookup = new Dictionary<FrameworkElement, IDraggable>();
        private IDisposable _rxMouseMove;

        private Panel _rootPanelInternal; //root panel used for capture mouse events

        /// <summary>
        /// Constructor
        /// </summary>
        protected DraggableBase()
        {
            var rp = Application.Current.RootVisual as Panel;
            if (rp == null) throw new DraggableException(null, "Application.Current.RootVisual must be of type Panel if default constructor is used.");
            RootPanel = rp;
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="rootPanel"></param>
        protected DraggableBase(Panel rootPanel)
        {
            if (rootPanel == null) throw new ArgumentNullException("rootPanel");

            RootPanel = rootPanel;
        }

        #region events

        /// <summary>
        /// Fires when drag operation begins
        /// </summary>
        protected event EventHandler<DraggableEventArgs> DragStarted;
        /// <summary>
        /// Fires when drag operation completes
        /// </summary>
        protected event EventHandler<DraggableEventArgs> DragCompleted;

        /// <summary>
        /// Called when drag operation starts
        /// </summary>
        /// <param name="draggable"></param>
        protected virtual void OnDragStarted(IDraggable draggable)
        {
            if (DragStarted != null) DragStarted(this, new DraggableEventArgs(draggable));
        }

        /// <summary>
        /// Called when drag operation completes
        /// </summary>
        /// <param name="draggable"></param>
        protected virtual void OnDragCompleted(IDraggable draggable)
        {
            if (DragCompleted != null) DragCompleted(this, new DraggableEventArgs(draggable));
        }

        #endregion

        /// <summary>
        /// A lookup table for valid click targets
        /// </summary>
        protected Dictionary<FrameworkElement, IDraggable> ClickTargetLookup
        {
            get { return _clickTargetLookup; }
        }

        /// <summary>
        /// Holds the mouse location at the beginning of the drag operation
        /// </summary>
        protected Point ClickPoint { get; set; }

        /// <summary>
        /// Holds the element captured during the drag operation
        /// </summary>
        protected FrameworkElement CapturedElement { get; set; }

        /// <summary>
        /// Holds the current transform delegate for the drag operation
        /// </summary>
        protected TransformDelegate CurrentTransformDelegate { get; set; }

        /// <summary>
        /// Holds the root panel used for drag operations
        /// </summary>
        protected Panel RootPanel
        {
            get { return _rootPanelInternal; }
            set
            {
                _rootPanelInternal = value;
                InitializeRootPanel();
            }
        }

        /// <summary>
        /// Wraps up initialization tasks for InitializeService() and it's overload.
        /// </summary>
        /// <returns></returns>
        private void InitializeRootPanel()
        {
            CurrentTransformDelegate = null;

            if (_rxMouseMove != null)
                _rxMouseMove.Dispose();

            //filtered mousemove events
            _rxMouseMove = Observable
                            .FromEvent<MouseEventArgs>(RootPanel, "MouseMove")
                            .Where(_ => CurrentTransformDelegate != null)
                            .Subscribe(RootPanelMouseMove);

            // panels needs to have a background brush assigned (even if transparent) in order
            // to track mouse movements
            if (RootPanel.Background == null)
                RootPanel.Background = new SolidColorBrush(Colors.Transparent);

        }

        /// <summary>
        /// Applies the applicable transformation to the window during the mousedown/move event.
        /// </summary>
        /// <param name="args"></param>
        private void RootPanelMouseMove(IEvent<MouseEventArgs> args)
        {
            if (!ClickTargetLookup.ContainsKey(CapturedElement)) return;
            var target = ClickTargetLookup[CapturedElement].DragElement;
            var targetPos = args.EventArgs.GetPosition(target);

            var transformGroup = target.RenderTransform as TransformGroup;
            if (transformGroup == null)
            {
                transformGroup = new TransformGroup();
                transformGroup.Children.Add(target.RenderTransform);
            }

            CurrentTransformDelegate(target, targetPos, transformGroup);
        }

        /// <summary>
        /// performs a relative transformation during drag operations
        /// </summary>
        /// <param name="target"></param>
        /// <param name="xy"></param>
        /// <param name="g"></param>
        protected void TransformDrag(FrameworkElement target, Point xy, TransformGroup g)
        {
            target.RelativeTransformPositionTo(new Point(xy.X - ClickPoint.X, xy.Y - ClickPoint.Y));
        }

        /// <summary>
        /// registers an IDraggable to the class
        /// </summary>
        /// <param name="draggableToRegister"></param>
        protected void RegisterDraggable(IDraggable draggableToRegister)
        {
            //window element required.
            if (draggableToRegister.DragElement == null)
                throw new DraggableException(draggableToRegister, "Window Element is null or otherwise not defined.");

            //assign drag handle if exists, otherwise default to the entire DragElement as begin draggable.
            var handle = draggableToRegister.DragHandle ?? draggableToRegister.DragElement;

            if (ClickTargetLookup.ContainsKey(handle)) return;

            handle.MouseLeftButtonDown += DragMouseLeftButtonDown;
            ClickTargetLookup.Add(handle, draggableToRegister);
        }

        /// <summary>
        /// Unregisters a IDraggable object from the class
        /// </summary>
        /// <param name="draggableToUnRegister"></param>
        protected void UnRegisterDraggable(IDraggable draggableToUnRegister)
        {
            var handle = draggableToUnRegister.DragHandle ?? draggableToUnRegister.DragElement;

            if (!ClickTargetLookup.ContainsKey(handle)) return;

            handle.MouseLeftButtonDown -= DragMouseLeftButtonDown;
            ClickTargetLookup.Remove(handle);
        }

        /// <summary>
        /// Called when the mouse left button down occurs on the drag target
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected virtual void DragMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            CurrentTransformDelegate = TransformDrag;
            FinalizeLeftMouseButtonDown((FrameworkElement)sender, e);
        }

        /// <summary>
        /// Called to finalize left button down activity
        /// </summary>
        /// <param name="element"></param>
        /// <param name="e"></param>
        protected virtual void FinalizeLeftMouseButtonDown(FrameworkElement element, MouseEventArgs e)
        {
            var draggable = ClickTargetLookup[element];
            if (!draggable.DragEnabled) return;

            CapturedElement = element;

            ClickPoint = e.GetPosition(draggable.DragElement);

            RootPanel.MouseLeftButtonUp += RootPanelMouseLeftButtonUp;
            RootPanel.MouseLeave += RootPanelMouseLeave;

            OnDragStarted(draggable);
            draggable.OnDragStarted();
        }

        #region LeftButtonUp Handler
        /// <summary>
        /// called when mouse left button is up on the root panel
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected virtual void RootPanelMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            e.Handled = true;
            FinalizeMouseButtonUp();
            CurrentTransformDelegate = null;
        }

        internal virtual void FinalizeMouseButtonUp()
        {
            try
            {
                RootPanel.MouseLeftButtonUp -= RootPanelMouseLeftButtonUp;
            }
            // ReSharper disable EmptyGeneralCatchClause
            catch
            // ReSharper restore EmptyGeneralCatchClause
            { }

            try
            {
                RootPanel.MouseLeave -= RootPanelMouseLeave;
            }
            // ReSharper disable EmptyGeneralCatchClause
            catch
            // ReSharper restore EmptyGeneralCatchClause
            { }
            finally
            {
                if (ClickTargetLookup.ContainsKey(CapturedElement))
                {
                    var draggable = ClickTargetLookup[CapturedElement];
                    OnDragCompleted(draggable);
                    draggable.OnDragEnded();
                }
            }
        }
        #endregion

        /// <summary>
        /// Called when mouse leaves the root panel
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected virtual void RootPanelMouseLeave(object sender, MouseEventArgs e)
        {
            CurrentTransformDelegate = null;
            FinalizeMouseButtonUp();
        }

        #region IDisposable Members

        /// <summary>
        /// disposes resources
        /// </summary>
        public void Dispose()
        {
            ClickTargetLookup.Clear();
            _rxMouseMove.Dispose();
        }

        #endregion
    }
}
